Skip to content

feat: worktree-native workspace for parallel branch work#22

Merged
chubes4 merged 2 commits into
mainfrom
feat/workspace-worktrees
Apr 17, 2026
Merged

feat: worktree-native workspace for parallel branch work#22
chubes4 merged 2 commits into
mainfrom
feat/workspace-worktrees

Conversation

@chubes4
Copy link
Copy Markdown
Member

@chubes4 chubes4 commented Apr 17, 2026

Summary

Make the agent workspace worktree-native so multiple parallel sessions can cook features in the same repo without stepping on each other.

Today every session shares one git checkout per repo at ~/.datamachine/workspace/<repo>/. Two sessions branch-switching at the same time corrupts each other's work — the classic "agent stepped on itself" pattern.

After this change, each branch lives in its own directory:

```
~/.datamachine/workspace/
├── data-machine-code/ ← primary, hands-off by default
├── data-machine-code@fix-foo/ ← worktree for fix/foo
└── data-machine-code@feat-bar/ ← worktree for feat/bar
```

The <repo>@<branch-slug> naming makes the on-disk directory the handle. All existing read/write/git abilities transparently accept either form. No new flag plumbing through every ability.

What's in here

4 new abilities (CLI-only by default; list is REST-readable):

  • datamachine/workspace-worktree-add{repo, branch, from?} → creates <repo>@<slug> as a git worktree, checks out branch (creating off from if it does not exist locally; default origin/HEAD).
  • datamachine/workspace-worktree-list{repo?} → branches, heads, dirty counts. Distinguishes primary vs worktree.
  • datamachine/workspace-worktree-remove{repo, branch, force?} → refuses if dirty unless forced.
  • datamachine/workspace-worktree-prune — runs git worktree prune across all primaries.

1 new CLI subcommand:

```
wp datamachine-code workspace worktree add [--from=]
wp datamachine-code workspace worktree list [] [--format=json]
wp datamachine-code workspace worktree remove [--force]
wp datamachine-code workspace worktree prune
```

Handle resolverWorkspace::parse_handle() splits <repo>@<slug> into components. Called by Workspace::get_repo_path() and resolve_repo_path(), so WorkspaceReader and WorkspaceWriter work on either primary or worktree handles with zero call-site changes.

Branch slug/ becomes - (fix/foo-barfix-foo-bar). Anything outside [A-Za-z0-9._-] is stripped; runs of dashes collapse.

Primary read-only by default — mutating ops (git pull|add|commit|push) on a bare <repo> handle now require --allow-primary-mutation (CLI) / allow_primary_mutation: true (ability input). Codifies the discipline that the primary checkout tracks the deployed branch and shouldn't be touched by parallel agents.

Policy updates:

  • clone rejects @-suffixed names — that suffix is reserved for worktrees.
  • remove refuses to delete a primary that has linked worktrees attached (with helpful error listing them).
  • git push only enforces fixed_branch on the primary checkout. Worktrees may push any branch — that's the whole point of having them.

Tests

  • tests/smoke-worktree-handles.php — pure-PHP unit smoke for parse_handle() and slugify_branch(). No WP bootstrap required. 32/32 passing.
  • tests/TESTING.md — manual end-to-end checklist: clone → worktree add → edit → commit → push → remove, plus all the negative cases (dirty refuse, primary mutation guard, clone-with-@, primary-with-linked-worktrees).

Backward compatibility

All bare <repo> handles continue to resolve to the primary checkout. The only behavioral break is the new primary mutation guard — existing automation that mutates the primary needs --allow-primary-mutation. That's the intended default-deny.

Why this matters

Today, parallel agent work in the same repo is unsafe. After this lands, every Discord thread / kimaki session can have its own worktree handle for the repo it's editing. Multiple features cooked at once, isolated on disk, reviewed as separate PRs. Unblocks the autonomous-parallel-agent story end-to-end.

Design doc: see the "Workspace Worktrees Design" wiki article in the linked Intelligence site.

Add four new abilities — workspace-worktree-add|list|remove|prune — and a
matching "workspace worktree" CLI subcommand. The workspace now stores each
branch in its own directory at <workspace>/<repo>@<branch-slug>, so multiple
agent sessions can edit different branches of the same repo simultaneously
without stepping on each other.

All existing read/write/git abilities accept the new <repo>@<branch-slug>
handle alongside bare repo names; resolution happens through Workspace::
parse_handle() and a single resolve_repo_path() call site.

Mutating ops on the primary checkout (bare <repo>) now require
allow_primary_mutation=true / --allow-primary-mutation. The default-deny
codifies the "primary tracks the deployed branch, hands off" rule that
lived as informal discipline before. Worktrees are always allowed.

clone rejects @-suffixed names; remove refuses to delete a primary that has
linked worktrees attached; git push only enforces fixed_branch on the
primary, freeing worktrees to push any branch.

Includes pure-PHP smoke test for handle parsing/slugify (tests/
smoke-worktree-handles.php, 32/32 passing) and a manual end-to-end test
plan (tests/TESTING.md). AGENTS.md generator and README updated to teach
the worktree-first workflow.
Found and fixed during end-to-end testing on the intelligence-chubes4
site (DATAMACHINE_WORKSPACE_PATH=~/Developer):

- Ability callbacks now declare `array|WP_Error` return types so the
  primary-mutation guard (and other WP_Error returns) propagate cleanly
  instead of throwing a TypeError in WorkspaceAbilities::*.
- Add `--allow-primary-mutation` to the `workspace git` CLI docblock
  so WP-CLI accepts the flag.
- `workspace list` now surfaces `kind` (primary|worktree) and `repo`
  columns so the new on-disk model is visible.
- `workspace-worktree-list` allows `branch_slug` and `branch` to be
  null in its output schema (primaries have no slug; detached heads have
  no branch).
- New `external` field on worktree-list output. Worktrees outside the
  workspace path (e.g. /private/tmp/dm-worktrees/post-tracking from a
  manual `git worktree add`) now report their absolute path as the
  handle instead of a mangled relative slice.
- New `Workspace::resolve_default_base()` helper. `worktree add` defaults
  to `origin/HEAD` when --from is omitted (per design), with a safe
  fallback to plain HEAD if origin/HEAD is unset.

Live test summary on intelligence-chubes4:
- ✓ workspace list shows primary/worktree split
- ✓ worktree add data-machine-code test/smoke (created)
- ✓ worktree list discovers both in-workspace and external worktrees
- ✓ git status data-machine-code@test-smoke
- ✓ primary mutation guard fires (blocked by existing write_enabled
  policy first; both gates work)
- ✓ clone --name=foo@bar rejected (`@` reserved for worktrees)
- ✓ worktree remove cleans the directory and prunes the registry
- ✓ end-to-end on mcp-context-wporg with explicit --from=origin/main

Note: workspace read/write/edit/ls still hit the pre-existing Studio
WASM mount limitation when paths are outside the site folder. Tracked
elsewhere; unrelated to this PR.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant